package com.jin.webvideo.base.linkedfile.io;

import com.jin.webvideo.base.linkedfile.LinkedFile;
import com.jin.webvideo.base.linkedfile.LinkedItem;
import com.jin.webvideo.base.linkedfile.util.InputStreamUtils;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.List;

public abstract class LinkedFileInputStream extends InputStream {

    private LinkedFile linkedFile;
    // 当前输入流
    private InputStream inputStream;

    // 块索引，由block* 方法维护，其他方法中只获取值
    private int blockIdx = 0;
    // 块内索引由block* 方法维护，其他方法中只获取值
    private long blockInnerPoint = 0;

    private volatile boolean closed;

    private void ensureOpen() throws IOException {
        if (closed) {
            throw new IOException("Stream closed");
        }
    }

    public LinkedFileInputStream(LinkedFile linkedFile) throws IOException {
        this.linkedFile = linkedFile;
        linkedFile.loadDirZone();
    }

    @Override
    public long skip(long n) throws IOException {
        ensureOpen();
        long position = position();
        long newPosition = position + n;
        List<LinkedItem> zone = linkedFile.getDirZone();
        // 超出最大长度
        if (newPosition >= linkedFile.getLength()) {
            blockMove(zone.size() - this.blockIdx);
            return linkedFile.getLength() - position;
        }
        // 超过5%使用二分查找，
        long boundary = (long) (linkedFile.getLength() *0.05);
        // 寻找新位置所在的块
        int offset = 0;
        if (boundary < n) {
            int num = binaryLookup(newPosition);
            offset = num - this.blockIdx;
        }
        else {
            while (zone.get(blockIdx + offset).getEnd() <= newPosition) {
                offset++;
            }
        }

        // 移动到块
        blockMove(offset);
        // 设置块内偏移
        blockSkip(newPosition - currentBlock().getStart());
        return n;
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        ensureOpen();
        int available = blockRead(b, off, len);
        if (canNextBlock()) {
            nextBlock();
        }
        // 结束或缓冲区已满
        if (available == -1 || available == len) {
            if (available == -1) {
            }
            return available;
        }
        // 剩下的从下一个块读取， 若没有下一个块返回当前值
        int r = read(b, available, len - available);
        return r == -1 ? available : r;
    }

    @Override
    public int read() throws IOException {
        ensureOpen();
        int bt = blockRead();
        while (canNextBlock()) {
            nextBlock();
        }
        return bt;
    }

    /**
     * 返回当前总位置
     * @return
     * @throws IOException
     */
    public long position() throws IOException{
        ensureOpen();
        if (illegalBlockNum()) {
            throw new IOException();
        }
        return currentBlock().getStart() + this.blockInnerPoint;
    }

    @Override
    public void close() throws IOException {
        if (this.inputStream != null) {
            this.inputStream.close();
        }
        this.closed = true;
    }

    // 获取当前块
    private LinkedItem currentBlock() {
        return this.linkedFile.getDirZone().get(this.blockIdx);
    }

    private boolean illegalBlockNum(int num) {
        return num >= linkedFile.getDirZone().size();
    }
    private boolean illegalBlockNum() {
        return illegalBlockNum(this.blockIdx);
    }

    private boolean canNextBlock() {
        return !illegalBlockNum(this.blockIdx) && this.blockInnerPoint == currentBlock().getLength();
    }

    private void nextBlock() throws IOException {
        blockMove(1);
    }

    private void blockMove(int blockOffset) throws IOException {
        this.blockIdx += blockOffset;
        this.blockInnerPoint = 0;
        if (illegalBlockNum()) {
            return;
        }
        LinkedItem zone = currentBlock();
        zone.load();
        URI uri = zone.getProperties().getURI("uri");
        URI absURI = linkedFile.getPath().resolve(uri);
        if (this.inputStream != null) {
            this.inputStream.close();
        }
        InputStream stream = createInputStream(absURI);
        this.inputStream = new CacheInputStream(stream);
        this.inputStream.skip(zone.getOffset());
    }

    protected abstract InputStream createInputStream(URI uri) throws IOException;

    private long blockSkip(long n) throws IOException {
        long num = this.inputStream.skip(n);
        blockInnerPoint += num;
        return num;
    }

    private int blockRead(byte[] b, int offset, int len) throws IOException {
        if (illegalBlockNum()) {
            return -1;
        }
        // 未初始化inputStream
        if (this.inputStream == null) {
            blockMove(0);
        }
        int l = this.inputStream.read(b, offset, len);
        if (l != -1) {
            blockInnerPoint += l;
        };
        return l;
    }

    private int blockRead() throws IOException {
        if (illegalBlockNum()) {
            return -1;
        }
        // 未初始化inputStream
        if (this.inputStream == null) {
            blockMove(0);
        }
        int bt = this.inputStream.read();
        if (bt != -1) {
            blockInnerPoint++;
        }
        return bt;
    }

    private int binaryLookup(long position) {
        int start = this.blockIdx;
        List<LinkedItem> zone = linkedFile.getDirZone();
        int end = zone.size();
        while (start < end) {
            int mid = (start + end) / 2;
            LinkedItem item = zone.get(mid);
            if (item.getStart() <= position && item.getEnd() > position) {
                return mid;
            }
            if (item.getStart() > position) {
                end = mid;
            }
            if (item.getEnd() < position){
                start = mid;
            }
        }
        return -1;
    }
}
